[Salesforce] プラットフォームイベントを利用して重いフローの処理を最後にまわす
重いフローの取り扱い
取引先(Account)を作成・更新すると紐づいている全取引先責任者(Contact)のメール送信除外(HasOptedOutOfEmail)にチェックを入れるフローがあるとします。このフローは紐付くContactの件数が多いほど処理時間が長くなります。
処理の間、SalesforceはAccountに排他ロックをかけます。そのため、当該Accountに対する他の子レコード(例えば、商談(Opportunity))の作成・更新する処理が続いている場合、Accountに対するロックが取得できず、UNABLE_TO_LOCK_ROWエラーが発生する可能性が高まります1。
このような場合、プラットフォームイベントを利用することで、重いフロー処理を一時停止し、条件が整った後でイベントを発火して実行する(例えば、Opportunityの作成・更新処理が終わった後に実行する)ことが可能です。
プラットフォームイベントを定義する
まず、プラットフォームイベントを定義します。 プラットフォームイベントはカスタムオブジェクトと同様の仕組みでSalesforce上で簡単に定義できます。
1) Salesforceにて[設定] > [プラットフォームイベント]にアクセスします。
2) [新規プアットフォームイベント]をクリックし、次のように設定しました。
項目 | 設定値 |
---|---|
表示ラベル | Processed |
オブジェクト名 | Processed |
公開動作 | コミット後に公開 |
3) 作成したプラットフォームイベントにカスタム項目を設定します。データ型はチェックボックスを選びました。
カスタム項目を次のように設定しました。
項目 | 設定値 |
---|---|
項目の表示ラベル | Completed |
デフォルト値 | チェックなし |
項目名 | Completed |
説明 | 処理が完了したらTrueにするフラグ項目。 |
フローでプラットフォームイベントの発火を待つ
フローの完成版は次の通りです。
最初に一時停止ロジックを配置し、プロセスからコールされた場合は一時停止してプラットフォームイベントの発火を待ちます。プロセスからのコールでない場合は一時停止しません。その後の処理は共通で、引数で受け取ったAccountに紐づくContactのメール送信除外(HasOptedOutOfEmail)をTrueにセットします。
以下、作り方になります。
まず、一時停止ロジックを使うために、「自動起動フロー(トリガなし)」を選択します。
最初の要素として一時停止ロジックを配置し、一時停止条件とイベント再開条件を設定します。
一時停止条件では、変数(IsCalledByProcess
)がTrue
の時に一時停止する設定を行なっています。プロセスからこのフローをコールする場合には、IsCalledByProcess
変数にTrue
を設定して呼ぶことにより一時停止を発動させる仕組みにしています。
イベント再開条件では、プラットフォームイベント(Processed
)のComplete__c
にてTrue
を受信したら再開という設定を行なっています。
フローを呼び出すプロセスを用意する
定義したフローを呼び出すプロセスをプロセスビルダーで用意します。 ここでは、Accountにカスタム項目「オプトアウト実行(execOptOut__c)」を定義し、この項目にチェックが入った時にフローを呼び出すプロセスにしました。
プラットフォームイベントを発火する
準備ができましたので、プラットフォームイベントの発火を行なってみたいと思います。
ここでは、JavaScriptを使って次の段取りで処理を行いました。
- 特定Accountの
オプトアウト実行(execOptOut__c)
をTrueに更新し、フローを呼び出して一時停止させる。 - フローが一時停止中に当該Accountに紐づく200件のOpportunityを作成する。
- Opportunity作成が終わったらプラットフォームイベントを発火する。プラットフォームイベントの発火によりフローの一時停止が解除され、オプトアウト処理が実行される。
1でオプトアウトを実行するAccountには予めContactを2,000個紐付けておいて、10秒以上オプトアウト処理に時間がかかるようにしておきます。10秒ロックが取れないとUNABLE_TO_LOCK_ROWエラーが発生するため、Opportunity作成処理の後にオプトアウト処理を実行しないとUNABLE_TO_LOCK_ROWエラーが発生する状況を作っています。
コードは次の通りです。
const jsforce = require('jsforce'); const sf_account = '<Salesforceアカウント名>'; const sf_password = '<Salesforceパスワード>' + '<Salesforceセキュリティトークン>'; const sf_env = 'https://login.salesforce.com'; const targetAccountId = '<処理対象のAccountId>'; const closeDate = '<商談のCloseDate>'; let conn = new jsforce.Connection({ loginUrl: sf_env }); function login() { return new Promise(function(resolve, reject) { conn.login(sf_account, sf_password, function(err, res) { if (err) { reject(err); } resolve(); }); }); } function pre(obj) { return JSON.stringify(obj, null, 2); } async function execAll() { try { await login(); conn.sobject("Account").retrieve(targetAccountId, function(err, account) { if (err) { return console.error(err); } // オプトアウト実行にAccountを更新 conn.sobject("Account").update({ Id : account.Id, execOptOut__c : true, }, function(err, ret) { if (err || !ret.success) { return console.error(err); } // 商談作成 const opportunities = []; for (let i = 0; i < 200; i++) { opportunities.push({ AccountId: account.Id, Name: 'Opportunity #' + i, StageName: 'Closed Won', CloseDate: closeDate, }); } conn.sobject("Opportunity").create(opportunities, function(err, rets) { if (err) { return console.error(err); } for (let j=0; j < rets.length; j++) { if (rets[j].success) { console.log("Created record id : " + rets[j].id); } } // プラットフォームイベントを発火 conn.sobject('Processed__e').create({ Completed__c : true, }).then(function(ret) { if (!ret.success) { return console.error(pre(ret.error)); } console.log("fire platform event!"); }); }); }); }); return; } catch(err) { console.log(err); } } execAll();
プラットフォームイベントの発火はプラットフォームイベントをオブジェクトのように作成することで実現できます。
// プラットフォームイベントを発火 conn.sobject('Processed__e').create({ Completed__c : true, }).then(function(ret) { if (!ret.success) { return console.error(pre(ret.error)); } console.log("fire platform event!"); });
コードを実行するとOpportunityが作成完了するまでフローが一時停止し、プラットフォームイベント発火後にフローが再開して、UNABLE_TO_LOCK_ROWエラーが発生しないで処理が完了します。